/***** BEGIN LICENSE BLOCK *****
 * Version: CPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Common Public
 * License Version 1.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.eclipse.org/legal/cpl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2002 Benoit Cerrina <b.cerrina@wanadoo.fr>
 * Copyright (C) 2002-2004 Anders Bengtsson <ndrsbngtssn@yahoo.se>
 * Copyright (C) 2002-2004 Jan Arne Petersen <jpetersen@uni-bonn.de>
 * Copyright (C) 2004 Thomas E Enebo <enebo@acm.org>
 * Copyright (C) 2004 Stefan Matthias Aust <sma@3plus4.de>
 * Copyright (C) 2005 Charles O Nutter <headius@headius.com>
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the CPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the CPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package asruby.lang {


import asruby.anno.JRubyMethod;
import asruby.runtime.Arity;
import asruby.runtime.Block;
import asruby.runtime.CallbackFactory;
import asruby.runtime.Frame;
import asruby.runtime.MethodIndex;
import asruby.runtime.ObjectAllocator;
import asruby.runtime.ThreadContext;
import asruby.runtime.Visibility;
import asruby.runtime.builtin.IRubyObject;
import asruby.runtime.marshal.MarshalStream;
import asruby.runtime.marshal.UnmarshalStream;
import asruby.util.IdUtil;
import asruby.common.IRubyWarnings.ID;
import asruby.exceptions.RaiseException;
import asruby.internal.runtime.methods.CallConfiguration;
import asruby.internal.runtime.methods.DynamicMethod;
import asruby.runtime.ClassIndex;
import asruby.runtime.callback.Callback;

/**
 * @author  jpetersen
 */
public class RubyStruct extends RubyObject {
    private  var values:Array;

    /**
     * Constructor for RubyStruct.
     * @param runtime
     * @param rubyClass
     */
    public  function RubyStruct(runtime:Ruby, rubyClass:RubyClass){
        super(runtime, rubyClass);
    }

    public static function createStructClass(runtime:Ruby):RubyClass{
        // TODO: NOT_ALLOCATABLE_ALLOCATOR may be ok here, but it's unclear how Structs
        // work with marshalling. Confirm behavior and ensure we're doing this correctly. JRUBY-415
        var structClass: RubyClass =  runtime.defineClass("Struct", runtime.getObject(), ObjectAllocator.NOT_ALLOCATABLE_ALLOCATOR);
        runtime.setStructClass(structClass);
        structClass.index = ClassIndex.STRUCT;
        structClass.includeModule(runtime.getEnumerable());
        structClass.defineAnnotatedMethods(RubyStruct);

        return structClass;
    }
    
    public  function getNativeTypeIndex():int{
        return ClassIndex.STRUCT;
    }
    
    private static function getInternalVariable(type:RubyClass, internedName:String):IRubyObject{
        var structClass: RubyClass =  type.getRuntime().getStructClass();
        var variable:IRubyObject;

        while (type != null && type != structClass) {
            if ((variable = type.fastGetInternalVariable(internedName)) != null) {
                return variable;
            }

            type = type.getSuperClass();
        }

        return type.getRuntime().getNil();
    }

    private  function classOf():RubyClass{
        return getMetaClass() instanceof MetaClass ? getMetaClass().getSuperClass() : getMetaClass();
    }

    private  function modify():void{
        testFrozen("Struct is frozen");

        if (!isTaint() && getRuntime().getSafeLevel() >= 4) {
            throw getRuntime().newSecurityError("Insecure: can't modify struct");
        }
    }
    
    public  function hash(context:ThreadContext):RubyFixnum{
        var runtime: Ruby =  getRuntime();
        var h: int =  getMetaClass().getRealClass().hashCode();

        for ( var int:i = 0; i < values.length; i++) {
            h = (h << 1) | (h < 0 ? 1 : 0);
            h ^= RubyNumeric.num2long(values[i].callMethod(context, MethodIndex.HASH, "hash"));
        }
        
        return runtime.newFixnum(h);
    }

    private  function setByName(name:String, value:IRubyObject):IRubyObject{
        var member: RubyArray =  (RubyArray) getInternalVariable(classOf(), "__member__");

        assert !member.isNil() : "uninitialized struct";

        modify();

        for ( var int:i = 0,k=member.getLength(); i < k; i++) {
            if (member.eltInternal(i).asJavaString().equals(name)) {
                return values[i] = value;
            }
        }

        throw notStructMemberError(name);
    }

    private  function getByName(name:String):IRubyObject{
        var member: RubyArray =  (RubyArray) getInternalVariable(classOf(), "__member__");

        assert !member.isNil() : "uninitialized struct";

        for ( var int:i = 0,k=member.getLength(); i < k; i++) {
            if (member.eltInternal(i).asJavaString().equals(name)) {
                return values[i];
            }
        }

        throw notStructMemberError(name);
    }

    // Struct methods

    /** Create new Struct class.
     *
     * MRI: rb_struct_s_def / make_struct
     *
     */
    public static function newInstance(recv:IRubyObject, args:Array, block:Block):RubyClass{
        var name: String =  null;
        var nilName: Boolean =  false;
        var runtime: Ruby =  recv.getRuntime();

        if (args.length > 0) {
            var firstArgAsString: IRubyObject =  args[0].checkStringType();
            if (!firstArgAsString.isNil()) {
                name = ((RubyString)firstArgAsString).getByteList().toString();
            } else if (args[0].isNil()) {
                nilName = true;
            }
        }

        var member: RubyArray =  runtime.newArray();

        for ( var int:i = (name == null && !nilName) ? 0 : 1; i < args.length; i++) {
            member.append(runtime.newSymbol(args[i].asJavaString()));
        }

        var newStruct:RubyClass;
        var superClass: RubyClass =  (RubyClass)recv;

        if (name == null || nilName) {
            newStruct = RubyClass.newClass(runtime, superClass); 
            newStruct.setAllocator(STRUCT_INSTANCE_ALLOCATOR);
            newStruct.makeMetaClass(superClass.getMetaClass());
            newStruct.inherit(superClass);
        } else {
            if (!IdUtil.isConstant(name)) {
                throw runtime.newNameError("identifier " + name + " needs to be constant", name);
            }

            var type: IRubyObject =  superClass.getConstantAt(name);
            if (type != null) {
                var frame: Frame =  runtime.getCurrentContext().getCurrentFrame();
                runtime.getWarnings().warn(ID.STRUCT_CONSTANT_REDEFINED, frame.getFile(), frame.getLine(), "redefining constant Struct::" + name, name);
                superClass.remove_const(runtime.newString(name));
            }
            newStruct = superClass.defineClassUnder(name, superClass, STRUCT_INSTANCE_ALLOCATOR);
        }

        newStruct.index = ClassIndex.STRUCT;
        
        newStruct.fastSetInternalVariable("__size__", member.length());
        newStruct.fastSetInternalVariable("__member__", member);

        var callbackFactory: CallbackFactory =  runtime.callbackFactory(RubyStruct);
        newStruct.getSingletonClass().defineMethod("new", callbackFactory.getOptSingletonMethod("newStruct"));
        newStruct.getSingletonClass().defineMethod("[]", callbackFactory.getOptSingletonMethod("newStruct"));
        newStruct.getSingletonClass().defineMethod("members", callbackFactory.getSingletonMethod("members"));

        // define access methods.
        for ( var int:i = (name == null && !nilName) ? 0 : 1; i < args.length; i++) {
            var memberName: String =  args[i].asJavaString();
            // if we are storing a name as well, index is one too high for values
            var index: int =  (name == null && !nilName) ? i : i - 1;
            newStruct.addMethod(memberName, new DynamicMethod(newStruct, Visibility.PUBLIC, CallConfiguration.NO_FRAME_NO_SCOPE) {
                public  function call(context:ThreadContext, self:IRubyObject, clazz:RubyModule, name:String, args:Array, block:Block):IRubyObject{
                    Arity.checkArgumentCount(self.getRuntime(), args, 0, 0);
                    return ((RubyStruct)self).get(index);
                }

                public  function call(context:ThreadContext, self:IRubyObject, clazz:RubyModule, name:String):IRubyObject{
                    return ((RubyStruct)self).get(index);
                }

                public  function dup():DynamicMethod{
                    return this;
                }
            });
            newStruct.addMethod(memberName + "=", new DynamicMethod(newStruct, Visibility.PUBLIC, CallConfiguration.NO_FRAME_NO_SCOPE) {
                public  function call(context:ThreadContext, self:IRubyObject, clazz:RubyModule, name:String, args:Array, block:Block):IRubyObject{
                    Arity.checkArgumentCount(self.getRuntime(), args, 1, 1);
                    return ((RubyStruct)self).set(args[0], index);
                }

                public  function call(context:ThreadContext, self:IRubyObject, clazz:RubyModule, name:String, arg:IRubyObject):IRubyObject{
                    return ((RubyStruct)self).set(arg, index);
                }

                public  function dup():DynamicMethod{
                    return this;
                }
            });
        }
        
        if (block.isGiven()) {
            // Struct bodies should be public by default, so set block visibility to public. JRUBY-1185.
            block.getBinding().setVisibility(Visibility.PUBLIC);
            block.yield(runtime.getCurrentContext(), null, newStruct, newStruct, false);
        }

        return newStruct;
    }

    /** Create new Structure.
     *
     * MRI: struct_alloc
     *
     */
    public static function newStruct(recv:IRubyObject, args:Array, block:Block):RubyStruct{
        var struct: RubyStruct =  new RubyStruct(recv.getRuntime(), (RubyClass) recv);

        var size: int =  RubyNumeric.fix2int(getInternalVariable((RubyClass) recv, "__size__"));

        struct.values = new IRubyObject[size];

        for ( var int:i = 0; i < size; i++) {
            struct.values[i] = recv.getRuntime().getNil();
        }

        struct.callInit(args, block);

        return struct;
    }

    public  function initialize(args:Array, unusedBlock:Block):IRubyObject{
        modify();

        var size: int =  RubyNumeric.fix2int(getInternalVariable(getMetaClass(), "__size__"));

        if (args.length > size) {
            throw getRuntime().newArgumentError("struct size differs (" + args.length +" for " + size + ")");
        }

        for ( var int:i = 0; i < args.length; i++) {
            values[i] = args[i];
        }

        return getRuntime().getNil();
    }
    
    public static function members(recv:IRubyObject, block:Block):RubyArray{
        var member: RubyArray =  (RubyArray) getInternalVariable((RubyClass) recv, "__member__");

        assert !member.isNil() : "uninitialized struct";

        var result: RubyArray =  recv.getRuntime().newArray(member.getLength());
        for ( var int:i = 0,k=member.getLength(); i < k; i++) {
            result.append(recv.getRuntime().newString(member.eltInternal(i).asJavaString()));
        }

        return result;
    }

    public  function members():RubyArray{
        return members(classOf(), Block.NULL_BLOCK);
    }
    
    public  function select(context:ThreadContext, block:Block):RubyArray{
        var array: RubyArray =  RubyArray.newArray(context.getRuntime());
        
        for ( var int:i = 0; i < values.length; i++) {
            if (block.yield(context, values[i]).isTrue()) {
                array.append(values[i]);
            }
        }
        
        return array;
    }

    public  function set(value:IRubyObject, index:int):IRubyObject{
        var member: RubyArray =  (RubyArray) getInternalVariable(classOf(), "__member__");

        assert !member.isNil() : "uninitialized struct";

        modify();

        return values[index] = value;
    }

    private  function notStructMemberError(name:String):RaiseException{
        return getRuntime().newNameError(name + " is not struct member", name);
    }

    public  function get(index:int):IRubyObject{
        var member: RubyArray =  (RubyArray) getInternalVariable(classOf(), "__member__");

        assert !member.isNil() : "uninitialized struct";

        return values[index];
    }

    public  function copySpecialInstanceVariables(clone:IRubyObject):void{
        var struct: RubyStruct =  (RubyStruct)clone;
        struct.values = new IRubyObject[values.length];
        System.arraycopy(values, 0, struct.values, 0, values.length);
    }

    public  function op_equal(context:ThreadContext, other:IRubyObject):IRubyObject{
        if (this == other) return getRuntime().getTrue();
        if (!(other instanceof RubyStruct)) return getRuntime().getFalse();
        if (getMetaClass().getRealClass() != other.getMetaClass().getRealClass()) return getRuntime().getFalse();
        
        var runtime: Ruby =  getRuntime();
        var otherStruct: RubyStruct =  (RubyStruct)other;
        for ( var int:i = 0; i < values.length; i++) {
            if (!equalInternal(context, values[i], otherStruct.values[i]).isTrue()) return runtime.getFalse();
        }
        return runtime.getTrue();
    }
    
    public  function eql_p(context:ThreadContext, other:IRubyObject):IRubyObject{
        if (this == other) return getRuntime().getTrue();
        if (!(other instanceof RubyStruct)) return getRuntime().getFalse();
        if (getMetaClass() != other.getMetaClass()) return getRuntime().getFalse();
        
        var runtime: Ruby =  getRuntime();
        var otherStruct: RubyStruct =  (RubyStruct)other;
        for ( var int:i = 0; i < values.length; i++) {
            if (!eqlInternal(context, values[i], otherStruct.values[i])) return runtime.getFalse();
        }
        return runtime.getTrue();        
    }

    public  function to_s():IRubyObject{
        return inspect();
    }

    public  function inspect():IRubyObject{
        var member: RubyArray =  (RubyArray) getInternalVariable(classOf(), "__member__");

        assert !member.isNil() : "uninitialized struct";

        var sb: StringBuffer =  new StringBuffer(100);

        sb.append("#<struct ").append(getMetaClass().getRealClass().getName()).append(' ');

        for ( var int:i = 0,k=member.getLength(); i < k; i++) {
            if (i > 0) {
                sb.append(", ");
            }

            sb.append(member.eltInternal(i).asJavaString()).append("=");
            sb.append(values[i].callMethod(getRuntime().getCurrentContext(), "inspect"));
        }

        sb.append('>');

        return getRuntime().newString(sb.toString()); // OBJ_INFECT
    }

    public  function to_a():RubyArray{
        return getRuntime().newArray(values);
    }

    public  function size():RubyFixnum{
        return getRuntime().newFixnum(values.length);
    }

    public  function each(context:ThreadContext, block:Block):IRubyObject{
        for ( var int:i = 0; i < values.length; i++) {
            block.yield(context, values[i]);
        }

        return this;
    }

    public  function each_pair(context:ThreadContext, block:Block):IRubyObject{
        var member: RubyArray =  (RubyArray) getInternalVariable(classOf(), "__member__");

        assert !member.isNil() : "uninitialized struct";

        for ( var int:i = 0; i < values.length; i++) {
            block.yield(context, getRuntime().newArrayNoCopy(new IRubyObject[]{member.eltInternal(i), values[i]}));
        }

        return this;
    }

    public  function aref(key:IRubyObject):IRubyObject{
        if (key instanceof RubyString || key instanceof RubySymbol) {
            return getByName(key.asJavaString());
        }

        var idx: int =  RubyNumeric.fix2int(key);

        idx = idx < 0 ? values.length + idx : idx;

        if (idx < 0) {
            throw getRuntime().newIndexError("offset " + idx + " too large for struct (size:" + values.length + ")");
        } else if (idx >= values.length) {
            throw getRuntime().newIndexError("offset " + idx + " too large for struct (size:" + values.length + ")");
        }

        return values[idx];
    }

    public  function aset(key:IRubyObject, value:IRubyObject):IRubyObject{
        if (key instanceof RubyString || key instanceof RubySymbol) {
            return setByName(key.asJavaString(), value);
        }

        var idx: int =  RubyNumeric.fix2int(key);

        idx = idx < 0 ? values.length + idx : idx;

        if (idx < 0) {
            throw getRuntime().newIndexError("offset " + idx + " too large for struct (size:" + values.length + ")");
        } else if (idx >= values.length) {
            throw getRuntime().newIndexError("offset " + idx + " too large for struct (size:" + values.length + ")");
        }

        modify();
        return values[idx] = value;
    }
    
    // FIXME: This is copied code from RubyArray.  Both RE, Struct, and Array should share one impl
    // This is also hacky since I construct ruby objects to access ruby arrays through aref instead
    // of something lower.
    public  function values_at(args:Array):IRubyObject{
        var olen: Number =  values.length;
        var result: RubyArray =  getRuntime().newArray(args.length);

        for ( var int:i = 0; i < args.length; i++) {
            if (args[i] instanceof RubyFixnum) {
                result.append(aref(args[i]));
                continue;
            }

            long beglen[];
            if (!(args[i] instanceof RubyRange)) {
            } else if ((beglen = ((RubyRange) args[i]).begLen(olen, 0)) == null) {
                continue;
            } else {
                var beg: int =  (int) beglen[0];
                var len: int =  (int) beglen[1];
                var end: int =  len;
                for ( var int:j = 0; j < end; j++) {
                    result.append(aref(getRuntime().newFixnum(j + beg)));
                }
                continue;
            }
            result.append(aref(getRuntime().newFixnum(RubyNumeric.num2long(args[i]))));
        }

        return result;
    }

    public static function marshalTo(struct:RubyStruct, output:MarshalStream):void{
        output.registerLinkTarget(struct);
        output.dumpDefaultObjectHeader('S', struct.getMetaClass());

        var members: Array =  ((RubyArray) getInternalVariable(structOf(), "__member__")).getList();
        output.writeInt(members.size());

        for ( var int:i = 0; i < members.size(); i++) {
            var name: RubySymbol =  (RubySymbol) members.get(i);
            output.dumpObject(name);
            output.dumpObject(struct.values[i]);
        }
    }

    public static function unmarshalFrom(input:UnmarshalStream):RubyStruct{
        var runtime: Ruby =  input.getRuntime();

        var className: RubySymbol =  (RubySymbol) input.unmarshalObject();
        var rbClass: RubyClass =  pathToClass(runtime, className.asJavaString());
        if (rbClass == null) {
            throw runtime.newNameError("uninitialized constant " + className, className.asJavaString());
        }

        var mem: RubyArray =  members(rbClass, Block.NULL_BLOCK);

        var len: int =  input.unmarshalInt();
        var values: Array =  new IRubyObject[len];
        for ( var int:i = 0; i < len; i++) {
            values[i] = runtime.getNil();
        }
        var result: RubyStruct =  newStruct(rbClass, values, Block.NULL_BLOCK);
        input.registerLinkTarget(result);
        for ( var int:i = 0; i < len; i++) {
            var slot: IRubyObject =  input.unmarshalObject();
            if(!mem.eltInternal(i).toString().equals(slot.toString())) {
                throw runtime.newTypeError("struct " + rbClass.getName() + " not compatible (:" + slot + " for :" + mem.eltInternal(i) + ")");
            }
            result.aset(runtime.newFixnum(i), input.unmarshalObject());
        }
        return result;
    }

    private static function pathToClass(runtime:Ruby, path:String):RubyClass{
        // FIXME: Throw the right ArgumentError's if the class is missing
        // or if it's a module.
        return (RubyClass) runtime.getClassFromPath(path);
    }
    
    private static var STRUCT_INSTANCE_ALLOCATOR: ObjectAllocator =  new ObjectAllocator() {
        public  function allocate(runtime:Ruby, klass:RubyClass):IRubyObject{
            var instance: RubyStruct =  new RubyStruct(runtime, klass);
            
            instance.setMetaClass(klass);
            
            return instance;
        }
    };
    
    public  function initialize_copy(arg:IRubyObject):IRubyObject{
        if (this == arg) return this;
        var original: RubyStruct =  (RubyStruct) arg;
        
        values = new IRubyObject[original.values.length];
        System.arraycopy(original.values, 0, values, 0, original.values.length);

        return this;
    }
    
}
}